package simulation;

import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.TreeSet;

import streaming.Blacklist;
import streaming.Host;
import streaming.Overlay;
import streaming.StreamingConfig;

// Basically this is an event-based simulator...
// At every iteration, the simulator dequeues an event and handles it..
// This event could be either be a message delivered at a node or an
// actual event that periodically notifies the node about something: 
//For example, a hearbeat event will periodically inform the node to send heartbeats

public class Simulator {

	public int numSim;
	public String strSimulationConfig;
	public String strSimulationConfigCompl;

	private static Simulator _instance = new Simulator();
	public Random random;
	public PriorityQueue queue;
	double time;
	int previousSecond = 0;
	Integer intMessagesSimpleSimulation;
	Integer intDiagnosticRequestMessages;
	Integer intDiagnosticResponseMessages;
	Integer intOriginalChunksCounter;
	Integer intModifiedChunksCounter;
	Integer intTotalChunksSent;
	Integer intTotalChunksReceived;
	Integer intTotalUSetsSentTracker;

	public int originalChunksPerSecond[] = new int [(int)(Config.TIME + StreamingConfig.MCASTWINDOWSIZE) + 1];
	public int pollutedChunksPerSecond[] = new int [(int)(Config.TIME + StreamingConfig.MCASTWINDOWSIZE) + 1];
	public int pollutedPeersPerSecond[] = new int [(int)(Config.TIME + StreamingConfig.MCASTWINDOWSIZE) + 1];

	public int chunksSentWithoutCMPerSecond[] = new int [(int)(Config.TIME + StreamingConfig.MCASTWINDOWSIZE) + 1];
	public int chunksSentByCMPerSecond[] = new int [(int)(Config.TIME + StreamingConfig.MCASTWINDOWSIZE) + 1];
	public int uSetsSentPerSecond[] = new int [(int)(Config.TIME + StreamingConfig.MCASTWINDOWSIZE) + 1];
	public boolean isAttacker[] = new boolean [StreamingConfig.INITIAL_HOSTS_NUMBER+StreamingConfig.HOSTS_NUMBER_TO_INSERT_ON_DISTRIBUTION+1000];
	public Collection[] isPolluted = new HashSet [(int) ((StreamingConfig.MCASTRATE*Config.TIME)+1)];

	public int currentSimulationSecond = -1;
	public List<Integer> pollutedPeersInCurrentSimulationSecond = new ArrayList<Integer>();

	public long msecs_blacklist = 0;
	public long msecs1 = 0;
	public long msecs2 = 0;
	public long msecs3 = 0;
	public long msecs4 = 0;
	public long msecs5 = 0;
	public long msecs6 = 0;
	public long msecs7 = 0;
	public long msecs8 = 0;
	public long msecs_chunksToBeCompared = 0;
	public long chunksToBeComparedContains = 0;
	
	HashMap <Integer, Map<Integer, Integer>> pollutedPeers; // K - chunk id, V -> map of peers polluted


	public TreeSet<Blacklist> totalBlacklisted = new TreeSet<Blacklist>(
			new Comparator<Blacklist>() {
				public int compare(Blacklist o1, Blacklist o2) {
					if (o1.host.getId().compareTo(o2.host.getId())!=0){return o1.host.getId().compareTo(o2.host.getId());}
					if (o1.cid.compareTo(o2.cid)!=0){return o1.cid.compareTo(o2.cid);}
					return 0;
				}
			});
	
	
	
	
	private Simulator() {
		queue = new PriorityQueue();
		random = new Random();
		time = 0;
		this.setIntMessagesSimpleSimulation(new Integer(0));
		this.setIntDiagnosticRequestMessages(new Integer(0));
		this.setIntDiagnosticResponseMessages(new Integer(0));
		this.setIntOriginalChunksCounter(new Integer(0));
		this.setIntModifiedChunksCounter(new Integer(0));
		this.setIntTotalChunksReceived(new Integer(0));
		this.setIntTotalChunksSent(new Integer(0));
		this.setIntTotalUSetsSentTracker(new Integer(0));
		this.pollutedPeers = new HashMap<Integer, Map<Integer,Integer>>();
		for (int i = 0 ; i < (int)(Config.TIME + StreamingConfig.MCASTWINDOWSIZE); i++) {
			this.chunksSentWithoutCMPerSecond[i] = 0;
			this.chunksSentByCMPerSecond[i] = 0;
			this.pollutedPeersPerSecond[i]=0;
			this.pollutedChunksPerSecond[i]=0;
			this.originalChunksPerSecond[i]=0;
		}
		for (int i = 0; i < ((StreamingConfig.MCASTRATE*Config.TIME)+1); i++) {
			this.isPolluted[i] = new HashSet<Integer>();
		}
		currentSimulationSecond = -1;
		pollutedPeersInCurrentSimulationSecond = new ArrayList<Integer>();
	
//		this.singlePollutedPeers = new HashMap<Integer, Integer>();
	}

	public void reset() {
		queue = new PriorityQueue();
		random = new Random();
		time = 0;
		this.setIntMessagesSimpleSimulation(new Integer(0));
		this.setIntDiagnosticRequestMessages(new Integer(0));
		this.setIntDiagnosticResponseMessages(new Integer(0));
		this.setIntOriginalChunksCounter(new Integer(0));
		this.setIntModifiedChunksCounter(new Integer(0));
		this.setIntTotalChunksReceived(new Integer(0));
		this.setIntTotalChunksSent(new Integer(0));
		this.setIntTotalUSetsSentTracker(new Integer(0));
		this.pollutedPeers = new HashMap<Integer, Map<Integer,Integer>>();		
	    for (int i = 0 ; i < (int)(Config.TIME + StreamingConfig.MCASTWINDOWSIZE); i++){
	    	this.chunksSentWithoutCMPerSecond[i] = 0;
        	this.chunksSentByCMPerSecond[i] = 0;
			this.pollutedPeersPerSecond[i]=0;
			this.pollutedChunksPerSecond[i]=0;
			this.originalChunksPerSecond[i]=0;
	    }
	    for (int i = 0; i < StreamingConfig.INITIAL_HOSTS_NUMBER+StreamingConfig.HOSTS_NUMBER_TO_INSERT_ON_DISTRIBUTION; i++) {
			this.isAttacker[i] = false;
		}
	    for (int i = 0; i < ((StreamingConfig.MCASTRATE*Config.TIME)+1); i++) {
			this.isPolluted[i].clear();
		}
	    currentSimulationSecond = -1;
	    pollutedPeersInCurrentSimulationSecond = new ArrayList<Integer>();
	}
	
	
	public void generateConfigurationString( int pNumSim ) {
		
		this.numSim = pNumSim;
		
		// -------------------------------
		// GENERATING CONFIGURATION STRING
		// -------------------------------
		// PCT OF MALICIOUS PEER
		String attackerRatio = StreamingConfig.ATTACKERS_RATIO + "";
		if (StreamingConfig.ATTACKERS_RATIO == 0.0 || StreamingConfig.ATTACKERS_RATIO == 0.1 || StreamingConfig.ATTACKERS_RATIO == 0.2) {
			attackerRatio += "0";   // Adds a '0' on the string to make files better looking
		}
		// TYPE OF MALICIOUS PEER
		String polluterType;
		if ( StreamingConfig.ATTACK_TYPE == 3 )       { polluterType = "omite"  ; } 
		else { if ( StreamingConfig.ALWAYS_POLLUTE )  { polluterType = "sempre" ; } 
			   else                                   { polluterType = "aleat"  ; }
		}
		// CHURN
		String churnUsed = "semch";
		if (StreamingConfig.DYNAMIC_INSERTION_ON) {
			churnUsed = "comch" + 100 * StreamingConfig.HOSTS_NUMBER_TO_INSERT_ON_DISTRIBUTION / StreamingConfig.INITIAL_HOSTS_NUMBER;
		}
		// FINAL CONFIGURATION STRING
		this.strSimulationConfig = "["+ attackerRatio + "," + polluterType + "," + churnUsed +"," + StreamingConfig.INITIAL_HOSTS_NUMBER + "," + (int)StreamingConfig.CHUNK_SELECTION_INTERVAL + "] ";
		// -------------------------------

		this.strSimulationConfigCompl = strSimulationConfig + Util.rpad( numSim, 3 ) +"  ";
		
		System.out.println( "## Cfg str generated. Starting execution --> "+ strSimulationConfig );
	}
	
	
	public static Simulator getInstance() {
		return _instance;
	}

	public double getTime() {
		return time;
	}

	public Integer getTimeTrunc() {
		return (int) Math.floor(time);
	}
	
	public String time() {
		long msecs = (long) (time * 1000 + 0.5);

		return "" + msecs + ": ";
	}

	public String time(String name) {
		long msecs = (long) (time * 1000 + 0.5);

		return "" + msecs + ": " + name + ": ";
	}

	public void addEvent(double time, EventHandler handler) {
		Event ev = new Event(time, handler);
		if (this.time > time)
			System.out
					.println("FATAL LOGICAL ERROR... TO SCHEDULE AT AN ELAPSED INSTANT!!");
		queue.add(ev);
	}

	Event firstEvent() {
		return (Event) queue.poll();
	}

	
	
	private void preRun( int numSim ) {

		// ----------------------------------------------
		// DADOS_neighbors.txt
		// ----------------------------------------------

		String strAttacker;
		String strAttackerNeighbors;
		int qtAttackerNeighbors;
		String strSourceNeighbors;
		String msgNeighborsIDs;

		for (int i = 0; i < StreamingConfig.INITIAL_HOSTS_NUMBER; i++) {
			msgNeighborsIDs = "";
			qtAttackerNeighbors = 0;
			strAttackerNeighbors = "";
			strSourceNeighbors = "[---]";
			
			strAttacker = "[normal]";
			if ( isAttacker[i] == true ) {
				strAttacker = "[ATTACKER]";
			}
			
			Host h = Overlay.getInstance().getHost(i);
			for (int k = 0; k < h.getNeighborIds().length; k++) {

				// Neighbors IDs
				msgNeighborsIDs += h.getNeighborIds()[k] + ",";

				// Attacker Neighbors IDs
				if ( isAttacker[ h.getNeighborIds()[k] ] == true ) {
					strAttackerNeighbors += h.getNeighborIds()[k] + ",";
					qtAttackerNeighbors++;
				}

				// Source Neighbor String
				if ( h.getNeighborIds()[k] == 0 ) {
					strSourceNeighbors = "[viz-fonte]";
				}
			}

			String msgNeighbors = strSimulationConfigCompl + " Peer " + Util.lpad(i,4) + " "+ 
					Util.rpad(strAttacker, 10) + " " + 
					Util.rpad(strSourceNeighbors, 11) + " " + 
					"NEIGHBORS (" + Util.lpad(h.getNumberNeighbors(),2) + ") ==> [" + Util.rpad(msgNeighborsIDs,100) + "]  " +
					"[ATTACKER_NEIGHs (" + Util.lpad(qtAttackerNeighbors,2) + ") ==> " + Util.rpad(strAttackerNeighbors,40) + "]";
			
			FileLog.getInstance().saveFinalStatsInFile("FinalStats", "DADOS_neighbors", msgNeighbors + "\n" );
		}

		// ----------------------------------------------
		
	}
	
	
	
	
	// Loops through tasks
	public void run( int numSim ) {
		
		preRun( numSim );
		double lastTimeElapsedPrinted = 0.0;
		
		// RUN SIMULATION
		for (;;) {
			Event ev = firstEvent();

			if (ev == null) {
				return;
			}
			
			time = ev.time;
			
			/* If maximum time is arrived, ends the simulation */
			// if (time >= Config.TIME + (10 * StreamingConfig.MCASTWINDOWSIZE)){ // Added '1.1 * StreamingConfig.MCASTWINDOWSIZE' to make possible all data to be transmited to all nodes by the end of simulation
			if (time > Config.TIME + StreamingConfig.MCASTWINDOWSIZE + 1){
				System.out.println("## Maximum simulation time arrived. Curr.Time = " + Util.lpad( Util.to_char(time,2) ,6) + "." );
				return;
			}
			// System.out.println("ev???"+ev.handler) ;
			ev.handler.run(ev.time);
			
			
			
			
			
			// Logs time epased every 10 seconds.
			if ( time > 1  &&  (time % 10) == 0  &&  lastTimeElapsedPrinted+1 < time ) {
				lastTimeElapsedPrinted = time;
				System.out.println("[sim.time] " + Util.lpad( Util.to_char(time,2) ,6) + " secs elapsed...  " + 
				"Chunks received = " + Util.lpad( getIntOriginalChunksCounter(), 8) + ", Polluted = " + Util.lpad( getIntModifiedChunksCounter(), 7) + 
				"  (" +  
				Util.lpad( Util.to_char( ((double)getIntModifiedChunksCounter()*(double)100/(double)(getIntOriginalChunksCounter()+getIntModifiedChunksCounter())) ,2), 6)  + 
				"% )" );
			}

		}
	}
	
	public void addIntMessagesSimplesSimulation(){
		this.setIntMessagesSimpleSimulation(this.getIntMessagesSimpleSimulation() + 1);
	}
	
	public void addIntDiagnosticRequestMessages(){
		this.setIntDiagnosticRequestMessages(this.getIntDiagnosticRequestMessages() + 1);
	}
	
	public void addIntDiagnosticResponseMessages(){
		this.setIntDiagnosticResponseMessages(this.getIntDiagnosticResponseMessages() + 1);
	}
	
	public void addIntOriginalChunksCounter(){
		this.setIntOriginalChunksCounter(this.getIntOriginalChunksCounter() + 1);
	}
	
	public void addIntModifiedChunksCounter(){
		this.setIntModifiedChunksCounter(this.getIntModifiedChunksCounter() + 1);
	}
	
	public void addIntTotalChunksReceived(){
		this.setIntTotalChunksReceived(this.getIntTotalChunksReceived() + 1);
	}
	
	public void addIntTotalChunksSent(){
		this.setIntTotalChunksSent(this.getIntTotalChunksSent() + 1);
	}
	
	public void addIntTotalUSetsSentTracker() {
		this.setIntTotalUSetsSentTracker(this.getIntTotalUSetsSentTracker() + 1);
	}
	
	public Integer getIntMessagesSimpleSimulation() {
		return intMessagesSimpleSimulation;
	}

	public void setIntMessagesSimpleSimulation(Integer intMessagesSimpleSimulation) {
		this.intMessagesSimpleSimulation = intMessagesSimpleSimulation;
	}

	public Integer getIntDiagnosticRequestMessages() {
		return intDiagnosticRequestMessages;
	}

	public void setIntDiagnosticRequestMessages(Integer intDiagnosticRequestMessages) {
		this.intDiagnosticRequestMessages = intDiagnosticRequestMessages;
	}

	public Integer getIntDiagnosticResponseMessages() {
		return intDiagnosticResponseMessages;
	}

	public void setIntDiagnosticResponseMessages(
			Integer intDiagnosticResponseMessages) {
		this.intDiagnosticResponseMessages = intDiagnosticResponseMessages;
	}

	public Integer getIntOriginalChunksCounter() {
		return intOriginalChunksCounter;
	}

	public void setIntOriginalChunksCounter(Integer intOriginalChunksCounter) {
		this.intOriginalChunksCounter = intOriginalChunksCounter;
	}

	public Integer getIntModifiedChunksCounter() {
		return intModifiedChunksCounter;
	}

	public void setIntModifiedChunksCounter(Integer intModifiedChunksCounter) {
		this.intModifiedChunksCounter = intModifiedChunksCounter;
	}

	public Integer getIntTotalChunksSent() {
		return intTotalChunksSent;
	}

	public void setIntTotalChunksSent(Integer intTotalChunksSent) {
		this.intTotalChunksSent = intTotalChunksSent;
	}

	public Integer getIntTotalChunksReceived() {
		return intTotalChunksReceived;
	}

	public void setIntTotalChunksReceived(Integer intTotalChunksReceived) {
		this.intTotalChunksReceived = intTotalChunksReceived;
	}

	public HashMap<Integer, Map<Integer, Integer>> getPollutedPeers() {
		return pollutedPeers;
	}

	public void setPollutedPeers(
			HashMap<Integer, Map<Integer, Integer>> pollutedPeers) {
		this.pollutedPeers = pollutedPeers;
	}

	public Integer getIntTotalUSetsSentTracker() {
		return intTotalUSetsSentTracker;
	}

	public void setIntTotalUSetsSentTracker(Integer intTotalUSetsSentTracker) {
		this.intTotalUSetsSentTracker = intTotalUSetsSentTracker;
	}
	
	public void addTotalBlacklisted(Blacklist blacklist){
		this.totalBlacklisted.add(blacklist);
	}

}